/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.providers.calendar; import com.android.common.content.SyncStateContentProviderHelper; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.test.mock.MockContext; import android.test.suitebuilder.annotation.MediumTest; import android.text.TextUtils; import android.util.Log; import java.util.Arrays; import junit.framework.TestCase; public class CalendarDatabaseHelperTest extends TestCase { private static final String TAG = "CDbHelperTest"; private SQLiteDatabase mBadDb; private SQLiteDatabase mGoodDb; private DatabaseUtils.InsertHelper mBadEventsInserter; private DatabaseUtils.InsertHelper mGoodEventsInserter; @Override public void setUp() { mBadDb = SQLiteDatabase.create(null); assertNotNull(mBadDb); mGoodDb = SQLiteDatabase.create(null); assertNotNull(mGoodDb); } protected void bootstrapDbVersion50(SQLiteDatabase db) { // TODO remove the dependency on this system class SyncStateContentProviderHelper syncStateHelper = new SyncStateContentProviderHelper(); syncStateHelper.createDatabase(db); db.execSQL("CREATE TABLE Calendars (" + "_id INTEGER PRIMARY KEY," + "_sync_account TEXT," + "_sync_id TEXT," + "_sync_version TEXT," + "_sync_time TEXT," + // UTC "_sync_local_id INTEGER," + "_sync_dirty INTEGER," + "_sync_mark INTEGER," + // Used to filter out new rows "url TEXT," + "name TEXT," + "displayName TEXT," + "hidden INTEGER NOT NULL DEFAULT 0," + "color INTEGER," + "access_level INTEGER," + "selected INTEGER NOT NULL DEFAULT 1," + "sync_events INTEGER NOT NULL DEFAULT 0," + "location TEXT," + "timezone TEXT" + ");"); // Trigger to remove a calendar's events when we delete the calendar db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " + "BEGIN " + "DELETE FROM Events WHERE calendar_id = old._id;" + "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" + "END"); // TODO: do we need both dtend and duration? db.execSQL("CREATE TABLE Events (" + "_id INTEGER PRIMARY KEY," + "_sync_account TEXT," + "_sync_id TEXT," + "_sync_version TEXT," + "_sync_time TEXT," + // UTC "_sync_local_id INTEGER," + "_sync_dirty INTEGER," + "_sync_mark INTEGER," + // To filter out new rows // TODO remove NOT NULL when upgrade rebuilds events to have // true v50 schema "calendar_id INTEGER NOT NULL," + "htmlUri TEXT," + "title TEXT," + "eventLocation TEXT," + "description TEXT," + "eventStatus INTEGER," + "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," + "commentsUri TEXT," + "dtstart INTEGER," + // millis since epoch "dtend INTEGER," + // millis since epoch "eventTimezone TEXT," + // timezone for event "duration TEXT," + "allDay INTEGER NOT NULL DEFAULT 0," + "visibility INTEGER NOT NULL DEFAULT 0," + "transparency INTEGER NOT NULL DEFAULT 0," + "hasAlarm INTEGER NOT NULL DEFAULT 0," + "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," + "rrule TEXT," + "rdate TEXT," + "exrule TEXT," + "exdate TEXT," + "originalEvent TEXT," + "originalInstanceTime INTEGER," + // millis since epoch "lastDate INTEGER" + // millis since epoch ");"); db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (calendar_id);"); db.execSQL("CREATE TABLE EventsRawTimes (" + "_id INTEGER PRIMARY KEY," + "event_id INTEGER NOT NULL," + "dtstart2445 TEXT," + "dtend2445 TEXT," + "originalInstanceTime2445 TEXT," + "lastDate2445 TEXT," + "UNIQUE (event_id)" + ");"); // NOTE: we do not create a trigger to delete an event's instances upon update, // as all rows currently get updated during a merge. db.execSQL("CREATE TABLE DeletedEvents (" + "_sync_id TEXT," + "_sync_version TEXT," + "_sync_account TEXT," + "_sync_mark INTEGER" + // To filter out new rows ");"); db.execSQL("CREATE TABLE Instances (" + "_id INTEGER PRIMARY KEY," + "event_id INTEGER," + "begin INTEGER," + // UTC millis "end INTEGER," + // UTC millis "startDay INTEGER," + // Julian start day "endDay INTEGER," + // Julian end day "startMinute INTEGER," + // minutes from midnight "endMinute INTEGER," + // minutes from midnight "UNIQUE (event_id, begin, end)" + ");"); db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (startDay);"); db.execSQL("CREATE TABLE CalendarMetaData (" + "_id INTEGER PRIMARY KEY," + "localTimezone TEXT," + "minInstance INTEGER," + // UTC millis "maxInstance INTEGER," + // UTC millis "minBusyBits INTEGER," + // UTC millis "maxBusyBits INTEGER" + // UTC millis ");"); db.execSQL("CREATE TABLE BusyBits(" + "day INTEGER PRIMARY KEY," + // the Julian day "busyBits INTEGER," + // 24 bits for 60-minute intervals "allDayCount INTEGER" + // number of all-day events ");"); db.execSQL("CREATE TABLE Attendees (" + "_id INTEGER PRIMARY KEY," + "event_id INTEGER," + "attendeeName TEXT," + "attendeeEmail TEXT," + "attendeeStatus INTEGER," + "attendeeRelationship INTEGER," + "attendeeType INTEGER" + ");"); db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (event_id);"); db.execSQL("CREATE TABLE Reminders (" + "_id INTEGER PRIMARY KEY," + "event_id INTEGER," + "minutes INTEGER," + "method INTEGER NOT NULL" + " DEFAULT 0);"); db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (event_id);"); // This table stores the Calendar notifications that have gone off. db.execSQL("CREATE TABLE CalendarAlerts (" + "_id INTEGER PRIMARY KEY," + "event_id INTEGER," + "begin INTEGER NOT NULL," + // UTC millis "end INTEGER NOT NULL," + // UTC millis "alarmTime INTEGER NOT NULL," + // UTC millis "state INTEGER NOT NULL," + "minutes INTEGER," + "UNIQUE (alarmTime, begin, event_id)" + ");"); db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (event_id);"); db.execSQL("CREATE TABLE ExtendedProperties (" + "_id INTEGER PRIMARY KEY," + "event_id INTEGER," + "name TEXT," + "value TEXT" + ");"); db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (event_id);"); // Trigger to remove data tied to an event when we delete that event. db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " + "BEGIN " + "DELETE FROM Instances WHERE event_id = old._id;" + "DELETE FROM EventsRawTimes WHERE event_id = old._id;" + "DELETE FROM Attendees WHERE event_id = old._id;" + "DELETE FROM Reminders WHERE event_id = old._id;" + "DELETE FROM CalendarAlerts WHERE event_id = old._id;" + "DELETE FROM ExtendedProperties WHERE event_id = old._id;" + "END"); // Triggers to set the _sync_dirty flag when an attendee is changed, // inserted or deleted db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + "END"); db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" + "END"); db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + "END"); // Triggers to set the _sync_dirty flag when a reminder is changed, // inserted or deleted db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + "END"); db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" + "END"); db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + "END"); // Triggers to set the _sync_dirty flag when an extended property is changed, // inserted or deleted db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + "END"); db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" + "END"); db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " + "BEGIN " + "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" + "END"); } private void createVersion67EventsTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE Events (" + "_id INTEGER PRIMARY KEY," + "_sync_account TEXT," + "_sync_account_type TEXT," + "_sync_id TEXT," + "_sync_version TEXT," + "_sync_time TEXT," + // UTC "_sync_local_id INTEGER," + "_sync_dirty INTEGER," + "_sync_mark INTEGER," + // To filter out new rows "calendar_id INTEGER NOT NULL," + "htmlUri TEXT," + "title TEXT," + "eventLocation TEXT," + "description TEXT," + "eventStatus INTEGER," + "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," + "commentsUri TEXT," + "dtstart INTEGER," + // millis since epoch "dtend INTEGER," + // millis since epoch "eventTimezone TEXT," + // timezone for event "duration TEXT," + "allDay INTEGER NOT NULL DEFAULT 0," + "visibility INTEGER NOT NULL DEFAULT 0," + "transparency INTEGER NOT NULL DEFAULT 0," + "hasAlarm INTEGER NOT NULL DEFAULT 0," + "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," + "rrule TEXT," + "rdate TEXT," + "exrule TEXT," + "exdate TEXT," + "originalEvent TEXT," + // _sync_id of recurring event "originalInstanceTime INTEGER," + // millis since epoch "originalAllDay INTEGER," + "lastDate INTEGER," + // millis since epoch "hasAttendeeData INTEGER NOT NULL DEFAULT 0," + "guestsCanModify INTEGER NOT NULL DEFAULT 0," + "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," + "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," + "organizer STRING," + "deleted INTEGER NOT NULL DEFAULT 0," + "dtstart2 INTEGER," + //millis since epoch, allDay events in local timezone "dtend2 INTEGER," + //millis since epoch, allDay events in local timezone "eventTimezone2 TEXT," + //timezone for event with allDay events in local timezone "syncAdapterData TEXT" + //available for use by sync adapters ");"); } private void addVersion50Events() { // April 5th 1:01:01 AM to April 6th 1:01:01 mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," + "eventTimezone,allDay,calendar_id) " + "VALUES (1,1270454471000,1270540872000,'P10S'," + "'America/Los_Angeles',1,1);"); // April 5th midnight to April 6th midnight, duration cleared mGoodDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," + "eventTimezone,allDay,calendar_id) " + "VALUES (1,1270425600000,1270512000000,null," + "'UTC',1,1);"); // April 5th 1:01:01 AM to April 6th 1:01:01, recurring weekly (We only check for the // existence of an rrule so it doesn't matter if the day is correct) mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration," + "eventTimezone,allDay,rrule,calendar_id) " + "VALUES (2,1270454462000,1270540863000," + "'P10S','America/Los_Angeles',1," + "'WEEKLY:MON',1);"); // April 5th midnight with 1 day duration, if only dtend was wrong we wouldn't fix it, but // if anything else is wrong we clear dtend to be sure. mGoodDb.execSQL("INSERT INTO Events (" + "_id,dtstart,dtend,duration," + "eventTimezone,allDay,rrule,calendar_id)" + "VALUES (2,1270425600000,null,'P1D'," + "'UTC',1," + "'WEEKLY:MON',1);"); assertEquals(mBadDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2); assertEquals(mGoodDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2); } private void addVersion67Events() { // April 5th 1:01:01 AM to April 6th 1:01:01 mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," + "eventTimezone,eventTimezone2,allDay,calendar_id) " + "VALUES (1,1270454471000,1270540872000,'P10S'," + "1270454460000,1270540861000,'America/Los_Angeles','America/Los_Angeles',1,1);"); // April 5th midnight to April 6th midnight, duration cleared mGoodDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," + "eventTimezone,eventTimezone2,allDay,calendar_id) " + "VALUES (1,1270425600000,1270512000000,null," + "1270450800000,1270537200000,'UTC','America/Los_Angeles',1,1);"); // April 5th 1:01:01 AM to April 6th 1:01:01, recurring weekly (We only check for the // existence of an rrule so it doesn't matter if the day is correct) mBadDb.execSQL("INSERT INTO Events (_id,dtstart,dtend,duration,dtstart2,dtend2," + "eventTimezone,eventTimezone2,allDay,rrule,calendar_id) " + "VALUES (2,1270454462000,1270540863000," + "'P10S',1270454461000,1270540861000,'America/Los_Angeles','America/Los_Angeles',1," + "'WEEKLY:MON',1);"); // April 5th midnight with 1 day duration, if only dtend was wrong we wouldn't fix it, but // if anything else is wrong we clear dtend to be sure. mGoodDb.execSQL("INSERT INTO Events (" + "_id,dtstart,dtend,duration,dtstart2,dtend2," + "eventTimezone,eventTimezone2,allDay,rrule,calendar_id)" + "VALUES (2,1270425600000,null,'P1D',1270450800000,null," + "'UTC','America/Los_Angeles',1," + "'WEEKLY:MON',1);"); assertEquals(mBadDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2); assertEquals(mGoodDb.rawQuery("SELECT _id FROM Events;", null).getCount(), 2); } @MediumTest public void testUpgradeToVersion69() { // Create event tables createVersion67EventsTable(mBadDb); createVersion67EventsTable(mGoodDb); // Fill in good and bad events addVersion67Events(); // Run the upgrade on the bad events CalendarDatabaseHelper.upgradeToVersion69(mBadDb); Cursor badCursor = null; Cursor goodCursor = null; try { badCursor = mBadDb.rawQuery("SELECT _id,dtstart,dtend,duration,dtstart2,dtend2," + "eventTimezone,eventTimezone2,rrule FROM Events WHERE allDay=?", new String[] {"1"}); goodCursor = mGoodDb.rawQuery("SELECT _id,dtstart,dtend,duration,dtstart2,dtend2," + "eventTimezone,eventTimezone2,rrule FROM Events WHERE allDay=?", new String[] {"1"}); // Check that we get the correct results back assertTrue(compareCursors(badCursor, goodCursor)); } finally { if (badCursor != null) { badCursor.close(); } if (goodCursor != null) { goodCursor.close(); } } } @MediumTest public void testUpgradeToCurrentVersion() { // Create event tables bootstrapDbVersion50(mBadDb); bootstrapDbVersion50(mGoodDb); // Fill in good and bad events addVersion50Events(); // Run the upgrade on the bad events CalendarDatabaseHelper cDbHelper = new CalendarDatabaseHelper(new MockContext()); cDbHelper.mInTestMode = true; cDbHelper.onUpgrade(mBadDb, 50, CalendarDatabaseHelper.DATABASE_VERSION); Cursor badCursor = null; Cursor goodCursor = null; try { badCursor = mBadDb.rawQuery("SELECT _id,dtstart,dtend,duration," + "eventTimezone,rrule FROM Events WHERE allDay=?", new String[] {"1"}); goodCursor = mGoodDb.rawQuery("SELECT _id,dtstart,dtend,duration," + "eventTimezone,rrule FROM Events WHERE allDay=?", new String[] {"1"}); // Check that we get the correct results back assertTrue(compareCursors(badCursor, goodCursor)); } finally { if (badCursor != null) { badCursor.close(); } if (goodCursor != null) { goodCursor.close(); } } } private static final String SQLITE_MASTER = "sqlite_master"; private static final String[] PROJECTION = {"tbl_name", "sql"}; public void testSchemasEqualForAllTables() { CalendarDatabaseHelper cDbHelper = new CalendarDatabaseHelper(new MockContext()); cDbHelper.mInTestMode = true; bootstrapDbVersion50(mBadDb); cDbHelper.onCreate(mGoodDb); cDbHelper.onUpgrade(mBadDb, 50, CalendarDatabaseHelper.DATABASE_VERSION); // Check that for all tables, schema definitions are the same between updated db and new db. Cursor goodCursor = mGoodDb.query(SQLITE_MASTER, PROJECTION, null, null, null, null, "tbl_name,sql" /* orderBy */); Cursor badCursor = mBadDb.query(SQLITE_MASTER, PROJECTION, null, null, null, null, "tbl_name,sql" /* orderBy */); while (goodCursor.moveToNext()) { String goodTableName = goodCursor.getString(0); // Ignore tables that do not belong to calendar if (goodTableName.startsWith("sqlite_") || goodTableName.equals("android_metadata")) { continue; } // Ignore tables that do not belong to calendar String badTableName; do { assertTrue("Should have same number of tables", badCursor.moveToNext()); badTableName = badCursor.getString(0); } while (badTableName.startsWith("sqlite_") || badTableName.equals("android_metadata")); assertEquals("Table names different between upgraded schema and freshly-created scheme", goodTableName, badTableName); String badString = badCursor.getString(1); String goodString = goodCursor.getString(1); if (badString == null && goodString == null) { continue; } // Have to strip out some special characters and collapse spaces to // get reasonable output badString = badString.replaceAll("[()]", ""); goodString = goodString.replaceAll("[()]", ""); badString = badString.replaceAll(" +", " "); goodString = goodString.replaceAll(" +", " "); // And then split on commas and trim whitespace String[] badSql = badString.split(","); String[] goodSql = goodString.split(","); for (int i = 0; i < badSql.length; i++) { badSql[i] = badSql[i].trim(); } for (int i = 0; i < goodSql.length; i++) { goodSql[i] = goodSql[i].trim(); } Arrays.sort(badSql); Arrays.sort(goodSql); assertTrue("Table schema different for table " + goodCursor.getString(0) + ": <" + Arrays.toString(goodSql) + "> -- <" + Arrays.toString(badSql) + ">", Arrays.equals(goodSql, badSql)); } assertFalse("Should have same number of tables", badCursor.moveToNext()); } /** * Compares two cursors to see if they contain the same data. * * @return Returns true of the cursors contain the same data and are not null, false * otherwise */ private static boolean compareCursors(Cursor c1, Cursor c2) { if(c1 == null || c2 == null) { Log.d("CDBT","c1 is " + c1 + " and c2 is " + c2); return false; } int numColumns = c1.getColumnCount(); if (numColumns != c2.getColumnCount()) { Log.d("CDBT","c1 has " + numColumns + " columns and c2 has " + c2.getColumnCount()); return false; } if (c1.getCount() != c2.getCount()) { Log.d("CDBT","c1 has " + c1.getCount() + " rows and c2 has " + c2.getCount()); return false; } c1.moveToPosition(-1); c2.moveToPosition(-1); while(c1.moveToNext() && c2.moveToNext()) { for(int i = 0; i < numColumns; i++) { if(!TextUtils.equals(c1.getString(i),c2.getString(i))) { Log.d("CDBT", c1.getString(i) + "\n" + c2.getString(i)); return false; } } } return true; } }